﻿/*
 *Author: Jeff Liu 
 *
 *Under MIT License
 *jebberwocky@gmail.com
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CtpGglMap.Core;
using CtpGglMap.Impl;

namespace CtpGglMap.Util
{
    /// <summary>
    /// reference:
    /// http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google-maps
    /// </summary>
    public class GeoUtil
    {
        /// <summary>
        /// It is half of the earth circumference in pixels at zoom level 21. You can visualize it by thinking of full map. Full map size is 536870912 *
        /// 536870912 pixels. Center of the map in pixel coordinates is 268435456,268435456 which in latitude and longitude would be 0,0.
        /// </summary>
        public const double OFFSET = 268435456;

        /// <summary>
        /// OFFSET / Math.PI
        /// </summary>
        public static double RADIUS
        {
            get
            {
                return OFFSET / Math.PI;
            }
        }

        /// <summary>
        /// Convert latitude to pixel Y
        /// refer: Mercator projection
        /// http://en.wikipedia.org/wiki/Mercator_projection
        /// &
        /// http://svn.appelsiini.net/svn/javascript/trunk/google_maps_nojs/Google/Maps.php
        /// </summary>
        /// <param name="latitude"></param>
        /// <returns></returns>
        public static int  LatToY(double latitude)
        {
            //round($offset - $radius * log((1 + sin($lat * pi() / 180)) / (1 - sin($lat * pi() / 180))) / 2);
            return (int)Math.Round(
                OFFSET - RADIUS * 
                Math.Log((1+Math.Sin(latitude*Math.PI/180))/(1-Math.Sin(latitude*Math.PI/180)))/2);
        }

        /// <summary>
        /// Convert pixel Y to latitude
        /// </summary>
        /// <param name="Y"></param>
        /// <returns></returns>
        public static double YToLat(int Y)
        {
            double dy = Y * 1.0;
            //(pi() / 2 - 2 * atan(exp((round($y) - self::$offset) / self::$radius))) * 180 / pi(); 
            return (Math.PI / 2 - 2 * Math.Atan(Math.Exp((Math.Round(dy) - OFFSET) / RADIUS))) * 180 / Math.PI;
        }

        /// <summary>
        /// Convert longitude to pixel X
        /// refer:
        ///  http://svn.appelsiini.net/svn/javascript/trunk/google_maps_nojs/Google/Maps.php
        /// </summary>
        /// <param name="longtitude"></param>
        /// <returns></returns>
        public static int LongToX(double longtitude)
        {
            //round($offset + $radius * $lon * pi() / 180);     
            return (int)Math.Round(OFFSET + RADIUS * longtitude * Math.PI / 180);
        }

        /// <summary>
        /// Convert pixel X to longitude
        /// </summary>
        /// <param name="X"></param>
        /// <returns></returns>
        public static double XToLong(int X)
        {
            //((round($x) - self::$offset) / self::$radius) * 180/ pi(); 
            double dx = X * 1.0;
            return ((Math.Round(dx) - OFFSET) / RADIUS) * 180 / Math.PI;
        }

        /// <summary>
        /// Convert GeoPoint to PixelXY
        /// </summary>
        /// <param name="point">GeoPoint object</param>
        /// <returns>converted PixelXY</returns>
        public static PixelXY GeoPointToXY(GeoPoint point)
        {
            if (point != null)
                throw new ArgumentNullException("point can not be null");
            else
            {
                return new PixelXY(GeoUtil.LongToX(point.Lng), GeoUtil.LatToY(point.Lat));
            }
        }

        public static GeoPoint XYToGeoPoint(PixelXY xy)
        {
            if (xy != null)
                throw new ArgumentNullException("point could not be null");
            else
            {
                return new GeoPointImpl(GeoUtil.YToLat(xy.Y), GeoUtil.XToLong(xy.X));
            }
        }

        public static double adjustLngByPixel(double lng, int delta, int zoom)
        {
            //return self::XToLon(self::LonToX($lon) + ($delta << (21 - $zoom)));
            return XToLong(LongToX(lng) + (delta << (21 - zoom)));
        }

        public static double adjustLatByPixel(double lat, int delta, int zoom)
        {
            //return self::YToLat(self::LatToY($lat) + ($delta << (21 - $zoom)));
            return YToLat(LatToY(lat) + (delta << (21 - zoom)));
        }

        /// <summary>
        /// calculate the center from list of IMapItem
        /// @since: 1.0.0
        /// </summary>
        /// <param name="items"></param>
        /// <returns></returns>
        public static GeoPoint CalculateCenterFromMapItem(List<IMapItem> items)
        {
            int count = items.Count;
            double total_lat = 0.0;
            double total_lng = 0.0;
            foreach (IMapItem item in items)
            {
                if (item != null)
                {
                    if (item.GetType() == typeof(GMarker) || item.GetType().IsSubclassOf(typeof(GMarker)))
                    {
                        GMarker marker = item as GMarker;
                        total_lat += marker.Point.Lat;
                        total_lng += marker.Point.Lng;
                    }
                }
            }
            GeoPoint point = new GeoPointImpl(total_lat / count, total_lng / count);
            return point;
        }

        /// <summary>
        /// calculate the bound for list of IMapItem
        /// @since 1.0.0
        /// </summary>
        /// <param name="map"></param>
        /// <param name="zoom"></param>
        /// <returns></returns>
        public static GBounds CalculateMapBounds(StaticGMap map, int zoom)
        {
            int delta_x = map.Width / 2;
            int delta_y = map.Height / 2;
            double lat = map.Center.Lat;
            double lng = map.Center.Lng;
            double north = GeoUtil.adjustLatByPixel(lat, delta_y * -1, zoom);
            double south = GeoUtil.adjustLatByPixel(lat, delta_y, zoom);
            double west = GeoUtil.adjustLngByPixel(lng, delta_x * -1, zoom);
            double east = GeoUtil.adjustLngByPixel(lng, delta_x, zoom);

            GeoPoint nw = new GeoPointImpl(north, west);
            GeoPoint se = new GeoPointImpl(south, east);

            return new GBounds(
                new List<GeoPoint>()
                {
                    nw, se
                }
                );
        }

        /// <summary>
        /// Calculate the geo point from pixel 
        /// </summary>
        /// <param name="map"></param>
        /// <param name="zoom"></param>
        /// <param name="xy"></param>
        /// <returns></returns>
        public static GeoPoint CalculateGeoPointFromXY(StaticGMap map, int zoom, PixelXY xy)
        {
            int delta_x = map.Width / 2;
            int delta_y = map.Height / 2;
            int x = xy.X - delta_x;
            int y = xy.Y - delta_y;

            return new GeoPointImpl(
                GeoUtil.adjustLatByPixel(map.Center.Lat, y, zoom),
                GeoUtil.adjustLngByPixel(map.Center.Lng, x, zoom)
                );
        }

        /// <summary>
        /// get the zoom from list of items
        /// @since 1.0.0
        /// </summary>
        /// <param name="map"></param>
        /// <param name="items"></param>
        /// <returns></returns>
        public static int ZoomToFit(StaticGMap map, List<IMapItem> items)
        {
            int zoom = 21;
            bool found = false;
            GBounds mark_bounds = new GBounds(items);
            while (!found)
            {
                GBounds map_bounds = CalculateMapBounds(map, zoom);
                found = map_bounds.Contains(mark_bounds);
                zoom--;
            }
            return zoom;
        }

        public static int ZoomToFit(StaticGMap map, List<IMapItem> items, int delta)
        {
            return ZoomToFit(map, items)  + delta;
        }

        /// <summary>
        /// calculate the pixel point based on GeoPoint point with center of map
        /// </summary>
        /// <param name="point"></param>
        /// <param name="map"></param>
        /// <returns></returns>
        public static PixelXY CalculateRelativePixelXY(GeoPoint point, IMap map)
        {
            if (map!=null && map.GetType() == typeof(StaticGMap) || map.GetType().IsSubclassOf(typeof(StaticGMap)))
            {
                StaticGMap themap = (StaticGMap)map;
                int center_offset_x = (int)Math.Round((double)themap.Width / 2);
                int center_offset_y = (int)Math.Round((double)themap.Height / 2);

                int center_y = GeoUtil.LatToY(themap.Center.Lat);
                int center_x = GeoUtil.LongToX(themap.Center.Lng);

                int target_y = GeoUtil.LatToY(point.Lat);
                int target_x = GeoUtil.LongToX(point.Lng);

                Console.WriteLine("zoom:" + map.Zoom);

                int delta_x = (target_x - center_x) >> (21 - map.Zoom);
                int delta_y = (target_y - center_y) >> (21 - map.Zoom);

                int marker_x = center_offset_x + delta_x;
                int marker_y = center_offset_y + delta_y;

                return new PixelXY(marker_x, marker_y);
            }
            else
            {
                throw new ArgumentException();
            }
        }

        /// <summary>
        /// calculate the distance from point a to point b on the map
        /// </summary>
        /// <param name="a">from</param>
        /// <param name="b">to</param>
        /// <param name="map">map</param>
        /// <returns></returns>
        public static int CalculateDistance(GeoPoint a, GeoPoint b, IMap map)
        {
            if (a != null && b != null && map != null)
            {
                PixelXY _a = CalculateRelativePixelXY(a, map);
                PixelXY _b = CalculateRelativePixelXY(b, map);
                double _xp = Math.Pow((_a.X - _b.X), 2);
                double _yp = Math.Pow((_a.X - _b.Y), 2);
                double _ds = Math.Sqrt(_xp + _yp);
                int intd = Convert.ToInt32(_ds);
                Console.WriteLine("_a:"+_a+", _b:"+_b+", distance: " + intd);
                int d = intd;//(intd >> (21 - map.Zoom));
                return d;
            }
            else
                throw new ArgumentNullException();
        }
    }
}
